// index.js
const express = require('express')
const superagent = require('superagent')
const cheerio = require('cheerio')
const humps = require('humps')
const cors = require('cors')
const app = express()
const PORT = 8085

const { Pool } = require('pg')
// 这个步骤是创建连接池对象并连接数据库，而刚刚我们在 dos 命令下是直接连接数据库，这里我们为什么不直接连接呢？
// - 由于我们的服务器要经常跟数据库打交道，如果每次连接数据库都要创建连接，断开再释放连接，一个项目几十到上百个接口，如果都每个接口都这个干，那是相当费时和耗性能的。而连接池存放着一些创建好的连接，每次要访问数据库，就可以直接从连接池获取连接，用完再还给连接池，相当于一个借阅功能，很好的保证了性能。
const pool = new Pool({
  user: 'postgres', // 用户名
  password: 'postgres', // 密码
  host: 'localhost', // 主机
  port: 5432, // 端口
  database: 'sign_search' // 数据库名
})

// 处理跨域问题
app.use(cors())

// test
app.get('/', (req, res) => {
  res.json('Hello express')
})

// 查询类别列表
app.get('/sign/categories', async (req, res) => {
  try {
    // 查询类别
    const categoriesResult = await pool.query('SELECT * FROM t_category')
    // 下划线转驼峰
    const categories = humps.camelizeKeys(categoriesResult.rows)
    res.json(categories)
  } catch (error) {
    console.error(error)
  }
})

// 查询符号
app.get('/sign/list', async (req, res) => {
  try {
    const categoriesResult = await pool.query('SELECT * FROM t_category')
    const categories = categoriesResult.rows
    const list = []
    for (const item of categories) {
      const id = item.id
      // 查询该类别下的符号数量
      // ?： 表示 SQL 占位符，用于防止 SQL 注入
      const countResult = await pool.query('SELECT count(*) FROM t_category_sign WHERE category_id = $1', [id])
      // 根据类别 id 查询符号列表数据
      const signResult = await pool.query('SELECT s.* FROM ((SELECT * from t_category_sign WHERE category_id = $1) as cs LEFT JOIN t_sign s ON cs.sign_id = s.id) order by s.id', [id])

      // 下划线转驼峰
      const children = humps.camelizeKeys(signResult.rows)

      list.push({
        id,
        categoryNameZh: item.category_name_zh,
        categoryNameEn: item.category_name_en,
        categoryCount: countResult.rows[0].count,
        children
      })
    }

    res.json({
      list,
      message: '查询成功！'
    })
  } catch (error) {
    console.error(error)
  }
})

// express.json()：内置中间件，用于解析 json 数据，类似于 springMVC 中的 @RequestBody 注解
// 前端传过来的参数为什么还要解析呢？ 也很好解释，json 是字符串，需要解析成对象（实体）后才能使用
// 中间件放置位置有无要求？ 除了错误级别的中间件，其他的中间件，必须在路由之前进行配置
app.use(express.json())
// 新增符号
app.post('/sign/add', async (req, res) => {
  try {
    const { pId, sign, signName, signKeywords } = req.body
    // 校验传入的 pId，判断类别是否存在
    const categoriesResult = await pool.query('SELECT * FROM t_category WHERE id = $1', [pId])
    if (categoriesResult.rows.length) {
      // 判断符号是否已存在
      const querySignResult = await pool.query('SELECT * FROM t_sign WHERE sign = $1', [sign])
      const querySign = querySignResult.rows
      console.log(querySign)
      if (querySign.length) {
        res.json({
          message: `新增失败！原因：符号『${sign}』已存在，您可以尝试修改它。`
        })
      } else {
        // 新增符号数据
        // RETURNING *： 表示返回你操作的全部数据（这个功能设计得很棒）
        const signResult = await pool.query('INSERT INTO t_sign (sign, sign_name, sign_keywords) VALUES ($1, $2, $3) RETURNING *', [sign, signName, signKeywords])
        console.log(signResult)
        const signData = signResult.rows[0]
        // 中间表将类别和符号进行关联
        await pool.query('INSERT INTO t_category_sign (category_id, sign_id) VALUES ($1, $2)', [pId, signData.id])
        res.json({
          data: {
            pId,
            ...signData
          },
          message: '新增成功！'
        })
      }
    } else {
      res.json({
        message: '新增失败！原因：不存在的类别'
      })
    }
  } catch (error) {
    console.error(error)
  }
})

// 修改符号信息
app.put('/sign/edit/:id', async (req, res) => {
  try {
    // 如何区别 req.params、req.query 和 req.body 参数？
    // - req.params 的参数是客户端通过动态路由传参。例如：/sign/edit/123
    // - req.query 的参数是客户端通过路径携带过来的，类型为：x-www-form-urlencoded。例如：/sign/search?keyword=up
    // - req.body 的参数是客户端通过 json 字符串携带过来的，类型为：application/json
    const { id } = req.params
    const { sign, signName, signKeywords } = req.body
    // 判断符号所处的类别是否发生了改变（即 pId 是否改变），这里暂时不做该优化

    // 吐槽一下：这里占位符必须写成 $1，$2 的这种方式才可以，官方完全可以学学 MySQL，改成 ?, ? 的方式，以减少开发者的心智负担
    const signResult = await pool.query('UPDATE t_sign SET sign = $1, sign_name = $2, sign_keywords = $3 where id = $4 RETURNING *', [sign, signName, signKeywords, id])
    res.json({
      data: signResult.rows[0],
      message: signResult.rows.length ? '修改成功！' : '修改失败！原因：不存在的数据项。'
    })
  } catch (error) {
    console.error(error)
  }
})

// 删除符号
app.delete('/sign/delete/:id', async (req, res) => {
  try {
    const { id } = req.params
    // 解绑外键约束
    // 为什么要解绑？由于 t_category_sign 表使用 t_sign 的 id 作为字段，不解除绑定不能删除 t_sign 中的对应 id 项
    await pool.query('DELETE FROM t_category_sign where sign_id = $1', [id])
    // 删除数据
    const result = await pool.query('DELETE FROM t_sign where id = $1', [id])
    res.json({
      message: result.rowCount ? '删除成功！' : '删除失败！原因：不存在的数据项。'
    })
  } catch (error) {
    console.error(error)
  }
})

// 爬取符号数据
// 参考文章：https://zhuanlan.zhihu.com/p/507515358
app.get('/crawl', (req, res) => {
  // http://www.fhcha.com/emoji.html
  superagent.get('http://www.fhcha.com/emoji.html').end((err, superRes) => {
    if (err) {
      return res.send(err)
    }
    const list = []
    // 获取网页 html 内容，转成实现 jquery 接口的变量
    const $ = cheerio.load(superRes.text)
    $('.app-main-area .app-box').each((index, element) => {
      // 获取类别标题
      const categoryTitle = $(element).children('.app-title').html()
      if (!categoryTitle) return false
      // 获取中文名
      const categoryNameZh = $(categoryTitle).first().text()
      // 获取英文名，暂时使用中文拼音代替
      // 剔除开头的 '//www.fhcha.com/' 和末尾的 '.html'
      const categoryNameEn = $(categoryTitle)
        .attr('href')
        .replace(/(\/\/www\.fhcha\.com\/|\.html)/g, '')

      // 获取类别数量
      const categoryCount = parseInt(
        $(categoryTitle)
          .last()
          .text()
          .replace(/[\(\)]/g, '')
      )

      // 获取符号列表数据
      const children = []
      const signList = $(element).children('.fuhao').html()
      $(signList).each((i, v) => {
        if (!$(v).html()) return
        const signItem = $(v).children('span')
        children.push({
          sign: signItem.text(),
          signName: signItem.attr('id'),
          signKeywords: signItem.attr('id')
        })
      })

      list.push({
        categoryNameZh,
        categoryNameEn,
        categoryCount,
        children
      })
    })

    res.send({
      list,
      message: '获取成功！'
    })
  })
})

app.listen(PORT, () => {
  console.log(`The server is running at http://localhost:${PORT}.`)
})
